﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.ComponentModel;
using System.Drawing;
using System.Web.UI.HtmlControls;

namespace Timeline
{
    [ToolboxData("<{0}:Timeline runat=\"server\" id=\"timeline1\"/>")]
    public class Timeline : WebControl
    {
        #region Member variables

        private int _fromYear;
        private int _toYear;
        private bool _generateLabels = false;
        private int _bigYearIndicator = 5;
        private int _smallYearIndicator = 1;
        private TimelineValueCollection _values;
        private TimelineAdditionalIndicatorCollection _addtionalIndicators;
        private string _indicatorCssClass;
        private string _labelCssClass;
        private string _labelFirstCssClass;
        private string _labelLastCssClass;
        private string _labelArrowCssClass;
        private string _labelArrowFirstCssClass;
        private string _labelArrowLastCssClass;
        private string _onClickJS;
        private int _yearWidthFirstLabel;
        private int _yearWidthLastLabel;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets from year.
        /// </summary>
        /// <value>
        /// From year.
        /// </value>
        /// <exception cref="System.ArgumentException">Value cannot be lower then 0!</exception>
        [Bindable(true), Category("Values"), DefaultValue("1984"), Description("From year.")]
        public int FromYear
        {
            get { return _fromYear; }
            
            set 
            {
                if (value < 0)
                    throw new ArgumentException("Value cannot be lower then 0!");
                _fromYear = value;
            }
        }

        /// <summary>
        /// Gets or sets to year.
        /// </summary>
        /// <value>
        /// To year.
        /// </value>
        /// <exception cref="System.ArgumentException">Value cannot be lower then 0!</exception>
        [Bindable(true), Category("Values"), DefaultValue("2013"), Description("To year.")]
        public int ToYear
        {
            get { return _toYear; }
            
            set 
            {
                if (value < 0)
                    throw new ArgumentException("Value cannot be lower then 0!");

                _toYear = value; 
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [generate labels].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [generate labels]; otherwise, <c>false</c>.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("false"), Description("Generate year indicator labels.")]
        public bool GenerateLabels
        {
            get { return _generateLabels; }
            set { _generateLabels = value; }
        }

        /// <summary>
        /// Gets or sets the big year indicator.
        /// </summary>
        /// <value>
        /// The big year indicator.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("5"), Description("Number of years between big indicators.")]
        public int BigYearIndicator
        {
            get { return _bigYearIndicator; }
            
            set 
            {
                if (value < 0)
                    throw new ArgumentException("Value cannot be lower then 0!");

                _bigYearIndicator = value; 
            }
        }

        /// <summary>
        /// Gets or sets the small year indicator.
        /// </summary>
        /// <value>
        /// The small year indicator.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("1"), Description("Number of years between small indicators.")]
        public int SmallYearIndicator
        {
            get { return _smallYearIndicator; }

            set 
            {
                if (value < 0)
                    throw new ArgumentException("Value cannot be lower then 0!");

                _smallYearIndicator = value; 
            }
        }

        /// <summary>
        /// Gets or sets the values.
        /// </summary>
        /// <value>
        /// The values.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue(""), Description("Periods to mark on the timeline.")]
        public TimelineValueCollection Values
        {
            get
            {
                if (_values == null)
                    _values = new TimelineValueCollection();

                return _values;
            }

            set { _values = value; }
        }

        [Bindable(true), Category("Values"), DefaultValue(""), Description("Additional indicators to mark on the timeline.")]
        public TimelineAdditionalIndicatorCollection AdditionalIndicators
        {
            get
            {
                if (_addtionalIndicators == null)
                    _addtionalIndicators = new TimelineAdditionalIndicatorCollection();

                return _addtionalIndicators;
            }

            set { _addtionalIndicators = value; }
        }

        /// <summary>
        /// Gets or sets the indicator CSS class.
        /// </summary>
        /// <value>
        /// The indicator CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the indicator background.")]
        public string IndicatorCssClass
        {
            get { return _indicatorCssClass; }
            set { _indicatorCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label CSS class.
        /// </summary>
        /// <value>
        /// The label CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the label style.")]
        public string LabelCssClass
        {
            get { return _labelCssClass; }
            set { _labelCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label first CSS class.
        /// </summary>
        /// <value>
        /// The label first CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the first label style.")]
        public string LabelFirstCssClass
        {
            get { return _labelFirstCssClass; }
            set { _labelFirstCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label last CSS class.
        /// </summary>
        /// <value>
        /// The label last CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the last label style.")]
        public string LabelLastCssClass
        {
            get { return _labelLastCssClass; }
            set { _labelLastCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label arrow CSS class.
        /// </summary>
        /// <value>
        /// The label arrow CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the label arrow style.")]
        public string LabelArrowCssClass
        {
            get { return _labelArrowCssClass; }
            set { _labelArrowCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label arrow first CSS class.
        /// </summary>
        /// <value>
        /// The label arrow first CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the first label arrow style.")]
        public string LabelArrowFirstCssClass
        {
            get { return _labelArrowFirstCssClass; }
            set { _labelArrowFirstCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the label arrow last CSS class.
        /// </summary>
        /// <value>
        /// The label arrow last CSS class.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("20px"), Description("Css class for setting the last label arrow style.")]
        public string LabelArrowLastCssClass
        {
            get { return _labelArrowLastCssClass; }
            set { _labelArrowLastCssClass = value; }
        }

        /// <summary>
        /// Gets or sets the text displayed when the mouse pointer hovers over the Web server control.
        /// </summary>
        /// <returns>The text displayed when the mouse pointer hovers over the Web server control. The default is <see cref="F:System.String.Empty" />.</returns>
        [Bindable(true), Category("Values"), DefaultValue(""), Description("Tool tip (overridden by timeline value collection tooltip).")]
        public override string ToolTip
        {
            get { return base.ToolTip; }
            set { base.ToolTip = value; }
        }

        /// <summary>
        /// Gets or sets the on click js.
        /// </summary>
        /// <value>
        /// The on click js.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue(""), Description("OnClick javascript.")]
        public string OnClickJS
        {
            get { return _onClickJS; }
            set { _onClickJS = value; }
        }

        /// <summary>
        /// Gets or sets the year width first label.
        /// </summary>
        /// <value>
        /// The year width first label.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("0"), Description("Minimum number of years between the first year label and the second year label.")]
        public int YearWidthFirstLabel
        {
            get { return _yearWidthFirstLabel; }
            set { _yearWidthFirstLabel = (value > 0) ? value : 0; }
        }

        /// <summary>
        /// Gets or sets the year width last label.
        /// </summary>
        /// <value>
        /// The year width last label.
        /// </value>
        [Bindable(true), Category("Values"), DefaultValue("0"), Description("Minimum number of years between the second to last year label and the last year label.")]
        public int YearWidthLastLabel
        {
            get { return _yearWidthLastLabel; }
            set { _yearWidthLastLabel = (value > 0) ? value : 0; }
        }

        #endregion

        #region Override

        /// <summary>
        /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
        /// </summary>
        protected override void CreateChildControls()
        {
            //Check if entered values are correct
            if (FromYear > ToYear)
                throw new ArgumentException("FromYear must be smaller then ToYear!");

            if (BigYearIndicator < SmallYearIndicator)
                throw new ArgumentException("BigYearIndicator must be smaller then SmallYearIndicator!");

            //Generate label spacer
            if (GenerateLabels)
            {
                HtmlGenericControl divSpacer = new HtmlGenericControl("div");

                divSpacer.Style.Add(HtmlTextWriterStyle.Height, (new Unit(Height.Value * 0.66, Height.Type)).ToString());
                divSpacer.Style.Add(HtmlTextWriterStyle.Width, Width.ToString());

                Controls.Add(divSpacer);
            }

            //Values table div
            HtmlGenericControl divValues = new HtmlGenericControl("div");

            divValues.Attributes.Add("class", CssClass);
            divValues.Style.Add(HtmlTextWriterStyle.Height, (GenerateLabels ? new Unit(Height.Value / 3, Height.Type) : Height).ToString());
            divValues.Style.Add(HtmlTextWriterStyle.Width, Width.ToString());

            //Generate values table
            Table tblValues = new Table();
            TableRow tblValuesRow = new TableRow();
            TableCell tblValuesCell1;
            TableCell tblValuesCell2;

            tblValues.CellPadding = 0;
            tblValues.CellSpacing = 0;
            tblValues.Height = GenerateLabels ? new Unit(Height.Value / 3, Height.Type) : Height;
            tblValues.Width = Unit.Percentage(100);
            tblValues.ToolTip = ToolTip;
            tblValues.Style.Add("table-layout", "fixed"); //Needed for Chrome

            if (!String.IsNullOrEmpty(OnClickJS))
            {
                tblValues.Attributes.Add("onclick", OnClickJS);
                tblValues.Attributes.Add("onmouseover", "this.style.cursor='pointer';");
            }

            tblValues.Rows.Add(tblValuesRow);

            //Add begin table cell
            tblValuesCell1 = new TableCell();
            tblValuesCell1.Width = Unit.Pixel(5);
            tblValuesRow.Cells.Add(tblValuesCell1);

            //Loop years and create table cells
            for (int i = FromYear; i < ToYear + 1; i++)
            {
                //Add first indicator
                if (i == FromYear)
                {
                    tblValuesCell2 = GetIndicator(i, "", true, false);

                    if (tblValuesCell2 != null)
                        tblValuesRow.Cells.Add(tblValuesCell2);
                }

                //Add year cell
                tblValuesCell1 = new TableCell();
                tblValuesRow.Cells.Add(tblValuesCell1);

                //Assign correct background css and tooltip to cell
                TimelineValue tv = Values.Where(tVal => i >= tVal.FromYear && i <= tVal.ToYear).FirstOrDefault();

                if (tv != null)
                {
                    tblValuesCell1.CssClass = String.IsNullOrEmpty(tv.BackgroundCssClass) ? Values.BackgroundCssClass : tv.BackgroundCssClass;
                    tblValuesCell1.ToolTip = String.IsNullOrEmpty(tv.ToolTip) ? Values.ToolTip : tv.ToolTip;

                    if (!String.IsNullOrEmpty(tv.OnClickJS))
                    {
                        tblValuesCell1.Attributes.Add("onclick", tv.OnClickJS);
                        tblValuesCell1.Attributes.Add("onmouseover", "this.style.cursor='pointer';");
                    }
                    else
                    {
                        if (!String.IsNullOrEmpty(Values.OnClickJS))
                        {
                            tblValuesCell1.Attributes.Add("onclick", Values.OnClickJS);
                            tblValuesCell1.Attributes.Add("onmouseover", "this.style.cursor='pointer';");
                        }
                    }
                }

                //Add indicator
                tblValuesCell2 = GetIndicator(i + 1, tblValuesCell1.CssClass, false, i == ToYear - 1);

                //Copy attributes from year cell to indicator cell
                tblValuesCell2.ToolTip = tblValuesCell1.ToolTip;

                if(!String.IsNullOrEmpty(tblValuesCell1.Attributes["onclick"]))
                    tblValuesCell2.Attributes.Add("onclick", tblValuesCell1.Attributes["onclick"]);

                if(!String.IsNullOrEmpty(tblValuesCell1.Attributes["onmouseover"]))
                    tblValuesCell2.Attributes.Add("onmouseover", tblValuesCell1.Attributes["onmouseover"]);

                if (tblValuesCell2 != null)
                    tblValuesRow.Cells.Add(tblValuesCell2);
            }

            //Add end table cell
            tblValuesCell1 = new TableCell();
            tblValuesCell1.Width = Unit.Pixel(5);
            tblValuesRow.Cells.Add(tblValuesCell1);

            divValues.Controls.Add(tblValues);
            Controls.Add(divValues);

            base.CreateChildControls();
        }

        /// <summary>
        /// Renders the control to the specified HTML writer.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter" /> object that receives the control content.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            //Remove automatically generated span tag from controls collection (generated in base.Render)
            foreach (Control c in Controls)
                c.RenderControl(writer);
        }

        /// <summary>
        /// Gets or sets the width of the Web server control.
        /// </summary>
        /// <returns>A <see cref="T:System.Web.UI.WebControls.Unit" /> that represents the width of the control. The default is <see cref="F:System.Web.UI.WebControls.Unit.Empty" />.</returns>
        public override Unit Width
        {
            get
            {
                //If width isn't specified, return fixed values
                if(!base.Width.IsEmpty)
                    return base.Width;

                return Unit.Percentage(100);
            }

            set { base.Width = value; }
        }

        /// <summary>
        /// Gets or sets the height of the Web server control.
        /// </summary>
        /// <returns>A <see cref="T:System.Web.UI.WebControls.Unit" /> that represents the height of the control. The default is <see cref="F:System.Web.UI.WebControls.Unit.Empty" />.</returns>
        public override Unit Height
        {
            get
            {
                //If height isn't specified, return fixed values
                if(!base.Height.IsEmpty)
                    return base.Height;

                if (GenerateLabels)
                    return Unit.Pixel(54);

                return Unit.Pixel(18);
            }

            set { base.Height = value; }
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Gets the indicator.
        /// </summary>
        /// <param name="year">The year.</param>
        /// <param name="tableCellCssClass">The table cell CSS class.</param>
        /// <param name="firstIndicator">if set to <c>true</c> [first indicator].</param>
        /// <param name="lastIndicator">if set to <c>true</c> [last indicator].</param>
        /// <param name="toolTip">The tool tip.</param>
        /// <param name="onClickJS">The on click js.</param>
        /// <returns></returns>
        private TableCell GetIndicator(int year, string tableCellCssClass, bool firstIndicator, bool lastIndicator)
        {
            TableCell tblc = null;
            HtmlGenericControl divIndicator = null;
            TimelineAdditionalIndicator aI;
            HtmlGenericControl divIndicatorLabel;

            bool bigYearIndicator = false;

            //Add big year indicator
            if (year % BigYearIndicator == 0)
            {
                divIndicator = new HtmlGenericControl("div");
                divIndicator.Style.Add(HtmlTextWriterStyle.Height, Unit.Percentage(66).ToString());

                bigYearIndicator = true;
            }
            else
            {
                //Add small year indicator
                if (year % SmallYearIndicator == 0)
                {
                    divIndicator = new HtmlGenericControl("div");
                    divIndicator.Style.Add(HtmlTextWriterStyle.Height, Unit.Percentage(33).ToString());
                }
            }

            //Set other properties
            if (divIndicator != null)
            {
                tblc = new TableCell();
                tblc.Height = Unit.Percentage(100);
                tblc.VerticalAlign = VerticalAlign.Bottom;
                tblc.CssClass = tableCellCssClass;
                tblc.Width = Unit.Pixel(bigYearIndicator ? 2 : 1);

                tblc.Controls.Add(divIndicator);

                divIndicator.Attributes.Add("class", IndicatorCssClass);
                divIndicator.Style.Add(HtmlTextWriterStyle.Width, tblc.Width.ToString());

                //Generate labels
                if (GenerateLabels)
                {
                    //Add indicator label
                    if ((bigYearIndicator && year >= (FromYear + YearWidthFirstLabel) && year <= (ToYear - YearWidthLastLabel)) || firstIndicator || lastIndicator)
                        tblc.Controls.Add(GetIndicatorLabel(year.ToString(), 
                                                            firstIndicator ? LabelFirstCssClass : (lastIndicator ? LabelLastCssClass : LabelCssClass),
                                                            firstIndicator ? LabelArrowFirstCssClass : (lastIndicator ? LabelArrowLastCssClass : LabelArrowCssClass)));

                    //Add additional indicator label
                    aI = AdditionalIndicators.Where(aInd => aInd.Year == year).FirstOrDefault();

                    if (aI != null)
                    {
                        divIndicatorLabel = GetIndicatorLabel(aI.Text,
                                                                String.IsNullOrEmpty(aI.LabelCssClass) ? AdditionalIndicators.LabelCssClass : aI.LabelCssClass,
                                                                String.IsNullOrEmpty(aI.LabelArrowCssClass) ? AdditionalIndicators.LabelArrowCssClass : aI.LabelArrowCssClass);
                        
                        //Add tooltip
                        divIndicatorLabel.Attributes.Add("title", String.IsNullOrEmpty(aI.ToolTip) ? AdditionalIndicators.ToolTip : aI.ToolTip);

                        //Add click event
                        if (!String.IsNullOrEmpty(aI.OnClickJS))
                        {
                            divIndicatorLabel.Attributes.Add("onclick", aI.OnClickJS);
                            divIndicatorLabel.Attributes.Add("onmouseover", "this.style.cursor='pointer';");
                        }
                        else
                        {
                            if (!String.IsNullOrEmpty(AdditionalIndicators.OnClickJS))
                            {
                                divIndicatorLabel.Attributes.Add("onclick", AdditionalIndicators.OnClickJS);
                                divIndicatorLabel.Attributes.Add("onmouseover", "this.style.cursor='pointer';");
                            }
                        }

                        tblc.Controls.Add(divIndicatorLabel);
                    }
                }
            }

            return tblc;
        }

        /// <summary>
        /// Gets the indicator label.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <param name="labelCssClass">The label CSS class.</param>
        /// <param name="arrowCssClass">The arrow CSS class.</param>
        /// <returns></returns>
        private HtmlGenericControl GetIndicatorLabel(string text, string labelCssClass, string arrowCssClass)
        {
            HtmlGenericControl divIndicatorLabel1 = null;
            HtmlGenericControl divIndicatorLabel2 = null;
            HtmlGenericControl divIndicatorLabel3 = null;
            HtmlGenericControl divIndicatorLabelArrow = null;

            //Wrap absolute div in relative div, add label arrow div
            divIndicatorLabel1 = new HtmlGenericControl("div");
            divIndicatorLabel1.Style.Add(HtmlTextWriterStyle.Position, "relative");

            divIndicatorLabel2 = new HtmlGenericControl("div");
            divIndicatorLabel2.Style.Add(HtmlTextWriterStyle.Position, "absolute");
            divIndicatorLabel2.Style.Add(HtmlTextWriterStyle.Top, (new Unit(Height.Value * -1, Height.Type)).ToString());

            divIndicatorLabel3 = new HtmlGenericControl("div");
            divIndicatorLabel3.Attributes.Add("class", labelCssClass);
            divIndicatorLabel3.Controls.Add(new Literal() { Text = text });

            divIndicatorLabelArrow = new HtmlGenericControl("div");
            divIndicatorLabelArrow.Attributes.Add("class", arrowCssClass);

            divIndicatorLabel2.Controls.Add(divIndicatorLabel3);
            divIndicatorLabel2.Controls.Add(divIndicatorLabelArrow);

            divIndicatorLabel1.Controls.Add(divIndicatorLabel2);

            return divIndicatorLabel1;
        }

        #endregion
    }
}